//
// GazetteerBackend.cs
//
// Copyright (C) 2008 Lukas Lipka <lukaslipka@gmail.com>
//

using System;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;

using Dashboard;
using Dashboard.Util;

namespace Dashboard.Engine.Backends
{
	public class GazetteerBackend : Backend, IComponent
	{
		private AirportsGazetteer airports = null;
		private PlacesGazetteer places = null;
		private PhonesGazetteer phones = null;

		public void Initialize ()
		{
			string backends_path = Path.Combine (PathFinder.Installation, "Dashboard.Engine.Backends");
			string gazetteer_path = Path.Combine (backends_path, "Gazetteer");
			
			airports = new AirportsGazetteer();
			airports.Load (new FileStream (Path.Combine (gazetteer_path, "Airports.txt"), FileMode.Open, FileAccess.Read));

			places = new PlacesGazetteer ();
			places.Load (new FileStream (Path.Combine (gazetteer_path, "Places.txt"), FileMode.Open, FileAccess.Read));

			phones = new PhonesGazetteer ();
			phones.Load (new FileStream (Path.Combine (gazetteer_path, "Phones.txt"), FileMode.Open, FileAccess.Read));

			Engine.MessageBroker.Subscribe ("/event/*", typeof (ClueMessage), OnMessage);
		}

		public void Dispose ()
		{
			Engine.MessageBroker.Unsubscribe (OnMessage);
		}

		private void OnMessage (Message message)
		{
			Engine.Log.Debug ("Processing message");
			
			ClueMessage clue_message = message as ClueMessage;
			MatchMessage matches_message = new MatchMessage ();
			
			foreach (Clue clue in clue_message.Clues) {
				if (clue.Type != "db:location")
					continue;

				// FIXME: Add a way to map a city name to all of the below

				double lat = Double.Parse (clue.Properties ["db:latitude"].Value);
				double lng = Double.Parse (clue.Properties ["db:longitude"].Value);

				PlacesGazetteer.Place place = places.NearestCity (lat, lng);
				AirportsGazetteer.Airport airport = airports.NearestAirport (lat, lng);

				Match match = new Match ("db:location");
				match.Trigger = clue_message.Id;
				match.Properties.Add (new Property ("db:city", place.City));
				match.Properties.Add (new Property ("db:region", place.Region));
				match.Properties.Add (new Property ("db:country", place.Country));
				match.Properties.Add (new Property ("db:code", place.Code));
				match.Properties.Add (new Property ("db:latitude", clue.Properties ["db:latitude"].Value));
				match.Properties.Add (new Property ("db:longitude", clue.Properties ["db:longitude"].Value));

				// FIXME: Add this in a sanitized way
				double distance = Distance (place.Latitude, place.Longitude, airport.Latitude, airport.Longitude);

				match.Properties.Add (new Property ("db:airportname", StringFu.SanitizeText (airport.Name)));
				match.Properties.Add (new Property ("db:airportcity", StringFu.SanitizeText (airport.City)));
				match.Properties.Add (new Property ("db:airportdistance", "dc:double", distance.ToString ()));

				matches_message.Matches.Add (match);
			}

			if (matches_message.Matches.Count > 0)
				Engine.MessageBroker.Send (matches_message);
		}

		private double Distance (double lat1, double lng1, double lat2, double lng2)
		{
			// Compute distance between two places using
			// the Haversine formula.

			const double deg2rad = Math.PI / 180;
			const double earth_radius = 6371;
			
			double dlat = (lat2 - lat1) * deg2rad;
			double dlon = (lng2 - lng1) * deg2rad;
			
			double slat = Math.Sin (dlat / 2);
			double slng = Math.Sin (dlon / 2);

			double a = (slat * slat) + Math.Cos (lat1 * deg2rad) * Math.Cos (lat2 * deg2rad) * (slng * slng);
			double c = 2 * Math.Asin (Math.Min (1 , Math.Sqrt (a)));
			double d = Math.Round (earth_radius * c, 2);

			return d;
		}

		public string Name {
			get { return "Gazetteer"; }
		}

		public string Description {
			get { return ""; }
		}
	}
	
	public class AirportsGazetteer : List<AirportsGazetteer.Airport>
	{
		public void Load (FileStream fs)
		{
			StreamReader reader = new StreamReader (fs);

			string l = null;
			int i = 0;

			while ((l = reader.ReadLine ()) != null) {
				string[] vals = l.Split (':');

				if (String.IsNullOrEmpty (vals [2]))
					continue;

				double lat = 0;
				double lng = 0;

				if (vals [8] == "N")
					lat = Double.Parse (vals [5]) + (Double.Parse (vals [6]) / 60) + (Double.Parse (vals [7]) / 3600);
				else
					lat -= Double.Parse (vals [5]) + (Double.Parse (vals [6]) / 60) + (Double.Parse (vals [7]) / 3600);;

				if (vals [12] == "E")
					lng = Double.Parse (vals [9]) + (Double.Parse (vals [10]) / 60) + (Double.Parse (vals [11]) / 3600);
				else
					lng -= Double.Parse (vals [9]) + (Double.Parse (vals [10]) / 60) + (Double.Parse (vals [11]) / 3600);

				// FIXME: We could still track by city and country but
				// it is just too much work at the moment.
				if (lat == 0 && lng == 0)
				    continue;

				Airport airport = new Airport ();
				airport.Name = vals [2];
				airport.City = vals [3];
				airport.Country = vals [4];
				airport.Latitude = lat;
				airport.Longitude = lng;
				Add (airport);

				//Console.WriteLine ("{0} ({1}, {2}) --> {3} {4}",
				//vals[2], vals[3], vals[4], lat, lng); 

				i++;
			}

			reader.Dispose ();

			fs.Close ();
			fs.Dispose ();

			Engine.Log.Debug ("Loaded {0} airports", i);
		}

		public Airport NearestAirport (double latitude, double longitude)
		{
			Airport best_match = null;
			double best_dlat = 360;
			double best_dlng = 360;

			foreach (Airport location in this) {
				double dlat = Math.Abs (location.Latitude - latitude);
				double dlng = Math.Abs (location.Longitude - longitude);

				if ((dlat + dlng) < (best_dlat + best_dlng)) {
					best_match = location;
					best_dlat = dlat; best_dlng = dlng;
				}
			}

			return best_match;
		}

		public class Airport {
			public double Latitude;
			public double Longitude;
			public string Name;
			public string City;
			public string Country;
		}
	}

	public class PlacesGazetteer : List<PlacesGazetteer.Place>
	{
		public void Load (FileStream fs)
		{
			StreamReader reader = new StreamReader (fs);

			// Discard the first line since it only contains the
			// formatting info for the input file.
			reader.ReadLine ();

			string l = null;
			int i = 0;

			while ((l = reader.ReadLine ()) != null) {
				string[] vals = l.Split ('\t');

				// Place has no latitude/longitude info - discard it
				if (vals [8] == "\"&nbsp;\"" || vals [9] == "\"&nbsp;\"")
					continue;

				double lat = 0;
				double lng = 0;

				if (vals[8].EndsWith ("N")) 
					lat = Double.Parse (vals [8].Substring (0, vals [8].Length - 2));
				else
					lat -= Double.Parse (vals [8].Substring (0, vals [8].Length - 2));

				if (vals[9].EndsWith ("E")) 
					lng = Double.Parse (vals [9].Substring (0, vals [9].Length - 2));
				else
					lng -= Double.Parse (vals [9].Substring (0, vals [9].Length - 2));


				//Console.WriteLine ("{0} {1} --> {2} {3} == '{4}' '{5}' '{6}' '{7}'",
				//vals[8], vals[9], lng, lat, vals[1], vals[3], vals[4], vals[5]); 
				
				Place p = new Place ();
				p.Latitude = lat;
				p.Longitude = lng;
				p.City = vals [1];
				p.Region = vals [3];
				p.Country = vals [4];
				p.Code = vals [5].ToUpper ();
				Add (p);

				i++;
			}

			reader.Close();

			fs.Close ();
			fs.Dispose ();

			Engine.Log.Debug ("Loaded {0} cities", i);
		}

		public Place NearestCity (double latitude, double longitude)
		{
			Place best_match = null;
			double best_dlat = 360;
			double best_dlng = 360;

			foreach (Place location in this) {
				double dlat = Math.Abs (location.Latitude - latitude);
				double dlng = Math.Abs (location.Longitude - longitude);

				if ((dlat + dlng) < (best_dlat + best_dlng)) {
					best_match = location;
					best_dlat = dlat; best_dlng = dlng;
				}
			}

			return best_match;
		}

		public class Place {
			public double Latitude;
			public double Longitude;
			public string City;
			public string Region;
			public string Country;
			public string Code;
		}
	}

	public class PhonesGazetteer : List<PhonesGazetteer.PhoneArea>
	{
		public void Load (FileStream fs)
		{
			StreamReader reader = new StreamReader (fs);

			string l = null;
			int i = 0;

			while ((l = reader.ReadLine ()) != null) {
				string[] vals = l.Split ('\t');

				PhoneArea p = new PhoneArea ();
				p.Prefix = vals [0].Trim ();
				p.Name = vals [3].Trim ();
				Add (p);

				i++;
			}

			reader.Close();

			fs.Close ();
			fs.Dispose ();

			Engine.Log.Debug ("Loaded {0} phone areas", i);
		}
		
		public string GetAreaName (string prefix)
		{
			foreach (PhoneArea area in this) {
				if (prefix.Length < area.Prefix.Length)
					continue;

				if (String.Compare (prefix, 0, area.Prefix, 0, area.Prefix.Length) == 0)
					return area.Name;
			}
			
			return null;
		}

		public class PhoneArea {
			public string Prefix;
			public string Name;
		}
	}
}
